package mt.comm;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import mt.Order;

/**
 * Implementation of the {@link ClientComm} interface. This implementation is
 * based on the Observer Design Pattern. Parties interested in the events from
 * this class shall implement the {@link ClientCommEventListener} interface and
 * register that implementation using the
 * {@link #setObserver(ClientCommEventListener)} method.
 */
public class ClientCommImpl implements ClientComm {
	/**
	 * The default server port (value: 6789).
	 */
	public static final int DEFAULT_SERVER_PORT = 6789;
	/**
	 * The default server address (value: "localhost").
	 */
	public static final String DEFAULT_SERVER_ADDRESS = "localhost";
	/**
	 * Queue that contains the messages sent by the server.
	 */
	private BlockingQueue<ClientSideMessage> queue;
	/**
	 * The nickname of the connected user.
	 */
	private String nickname;
	/**
	 * The socket to connect to the servers
	 */
	private Socket clientSocket;

	/**
	 * Creates a new instance of {@link ClientCommImpl}
	 */
	public ClientCommImpl() {
		// according to BlockingQueue javadoc:
		// "BlockingQueue implementations are thread-safe."
		queue = new LinkedBlockingQueue<ClientSideMessage>();
	}

	@Override
	public void connect(String host, String nickname) throws IOException {
		this.nickname = nickname;
		// --- creating the socket --------------------------------------------
		clientSocket = createSocket();
		// --- starting listening for the server's replies
		Thread serverListener = new Thread(new Runnable() {
			@Override
			public void run() {
				while (true) {
					ObjectInputStream inStream = null;
					try {
						inStream = new ObjectInputStream(
								clientSocket.getInputStream());
					} catch (Throwable t) {
						break;
					}
					ClientSideMessage message = null;
					try {
						message = (ClientSideMessage) inStream.readObject();
					} catch (Throwable t) {
						continue;
					}
					queue.add(message);
				}
			}
		});
		serverListener.start();
		// --- send the connect message to the server
		sendToServer(new ServerSideMessageImpl(
				ServerSideMessage.Type.CONNECTED, nickname));
	}

	@Override
	public boolean isConnected() {
		if (clientSocket == null) {
			return false;
		}
		return clientSocket.isConnected();
	}

	@Override
	public void disconnect() {
		queue.clear();
		try {
			sendToServer(new ServerSideMessageImpl(
					ServerSideMessage.Type.DISCONNECTED, nickname));
			closeSocket();
		} catch (IOException e) {
		}
	}

	@Override
	public ClientSideMessage getNextMessage() {
		try {
			return queue.take();
		} catch (InterruptedException e) {
			return null;
		}
	}

	@Override
	public boolean hasNextMessage() {
		return queue.size() > 0;
	}

	@Override
	public void sendOrder(Order order) {
		try {
			sendToServer(new ServerSideMessageImpl(
					ServerSideMessage.Type.NEW_ORDER, nickname, order));
		} catch (IOException e) {
		}
	}

	/**
	 * Closes a given socket.
	 *
	 * @param clientSocket
	 *            The socket to be closed.
	 * @throws IOException
	 *             The exception that will be thrown by {@link Socket#close()}
	 *             when anything goes wrong.
	 */
	private void closeSocket() throws IOException {
		clientSocket.close();
		clientSocket = null;
	}

	/**
	 * Creates, connects and returns a new {@link Socket} instance, using the
	 * values from the {@link #DEFAULT_SERVER_ADDRESS} and
	 * {@link #DEFAULT_SERVER_PORT} constants.
	 * 
	 * @return A new instance of a properly configured and connected
	 *         {@link Socket} instance to be used to communicate with the
	 *         server.
	 * @throws UnknownHostException
	 *             See {@link Socket#Socket(String, int)}
	 * @throws IOException
	 *             See {@link Socket#Socket(String, int)}
	 */
	private Socket createSocket() throws UnknownHostException, IOException {
		return new Socket(DEFAULT_SERVER_ADDRESS, DEFAULT_SERVER_PORT);
	}

	/**
	 * Sends a {@link ServerSideMessage} to the server. This method requires and
	 * open and connected {@link Socket}.
	 * 
	 * @param clientSocket
	 *            A socket connected to the server to whom we want to send the
	 *            message.
	 * @param message
	 *            The message to be sent to the server.
	 * @throws IOException
	 *             If an I/O error occurs while writing to the streams.
	 */
	private void sendToServer(ServerSideMessage message) throws IOException {
		// --- effectively sending the message --------------------------------
		ObjectOutputStream out = new ObjectOutputStream(
				clientSocket.getOutputStream());
		out.writeObject(message);
		out.flush();
	}
}